package edu.nju.mutest;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.sh0nk.matplotlib4j.Plot;
import com.github.sh0nk.matplotlib4j.PythonExecutionException;
import edu.nju.mutest.util.SystemUtil;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * A demo for executing test suite against mutants
 */
public class MutantExecution {

    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            // Require param for specifying test suite.
            System.out.println("DemoMutantExecution: <testsuite_dir> <mutant_pool_dir>");
            return;
        }
        File tsDir = new File(args[0]), mutPoolDir = new File(args[1]);
        System.out.println("[LOG] Test suite dir: " + tsDir.getAbsolutePath());
        System.out.println("[LOG] Mutant pool dir: " + mutPoolDir.getAbsolutePath());

        // Locate all test suites
        List<File> tsFiles = SystemUtil.traverseFolder(tsDir, ".java");
        if (tsFiles.isEmpty()) {
            System.out.println("[LOG] Find no test files!");
            System.exit(0);
        }
        List<String> testMainClasses = new ArrayList<>();
        for (File f : tsFiles) {
            try {
                CompilationUnit cu = StaticJavaParser.parse(f);
                if (containsMainMethod(cu)) {
                    String className = f.getName().substring(0, f.getName().lastIndexOf("."));
                    Optional<PackageDeclaration> pd = cu.getPackageDeclaration();
                    if (pd.isPresent()) {
                        testMainClasses.add(pd.get().getName().asString() + '.' + className);
                    } else {
                        testMainClasses.add(className);
                    }
                }
            } catch (Exception e) {
                System.err.println(Arrays.toString(e.getStackTrace()));
            }
        }
        System.out.printf("[LOG] Locate %d test files%n", testMainClasses.size());

        // Locate all mutants
        File[] fns = mutPoolDir.listFiles();
        if (fns == null) {
            System.out.println("[LOG] Find no mutants!");
            System.exit(0);
        }
        List<File> mutDirs = Arrays.stream(fns)
                .filter(f -> !f.getName().startsWith("."))
                .collect(Collectors.toList());
        int mutNum = mutDirs.size();
        System.out.printf("[LOG] Locate %d mutants%n", mutNum);

        // Execute each test suite. And calculate mutation score for each test suite.
        System.out.println("[LOG] Start to execute mutants...");
        List<Object> killedCntList = new ArrayList<>();
        List<Object> scoreList = new ArrayList<>();
        Set<File> killedDir = new HashSet<>();
        for (String tm : testMainClasses) {
            int killedCnt = 0;
            // Execute each mutant
            for (File mutDir : mutDirs) {
                String mutName = mutDir.getName();
                System.out.println("[LOG] -------------------------------------------------");
                System.out.println("[LOG] Compile File: " + SystemUtil.compileFiles(mutDir, mutDir, tsDir));
                System.out.println("[LOG] Execute " + mutName);
                if (SystemUtil.execCmd("java", "-cp", mutDir.getAbsolutePath(), tm) != 0) {
                    killedCnt++;
                    killedDir.add(mutDir);
                    System.out.println("[LOG] Killed " + mutName);
                } else {
                    System.out.println("[LOG] Survived " + mutName);
                }
            }
            // Calculate mutation score
            double score = calScore(killedCnt, mutNum);
            killedCntList.add(killedCnt);
            scoreList.add(score);
            System.out.println("\n\n[LOG] ======================================================");
            System.out.printf("[LOG] Stats: %d/%d(#killed/#total), score=%.2f%n%n", killedCnt, mutNum, score);
        }
        // calculate the score for all test suites.
        testMainClasses.add("all_test_suites");
        killedCntList.add(killedDir.size());
        scoreList.add(calScore(killedDir.size(), mutNum));
        // Organize results and save to .csv file.
        List<List<?>> result = List.of(
                testMainClasses,
                killedCntList,
                Collections.nCopies(testMainClasses.size(), mutNum),
                scoreList
        );
        SystemUtil.writeCSVFile(result, "result.csv");
        // Draw graphs with matplotlib4j.
        Plot plt = Plot.create();
        List<Double> x = (List<Double>)result.get(3);
        List<Double> y = new ArrayList<>();
        for (int i = 0; i < x.size(); i++) {
            Double a = x.get(i);
            while (a > 0) {
                y.add((double)i);
                a -= 1;
            }
        }
        plt.hist().add(y).bins(x.size() * 2).stacked(true);
        try {
            plt.show();
        } catch (PythonExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean containsMainMethod(CompilationUnit cu) {
        return cu.findAll(MethodDeclaration.class)
                .stream()
                .anyMatch(md -> md.getNameAsString().equals("main")
                        && md.getModifiers().stream().anyMatch(m -> m.getKeyword() == Modifier.Keyword.PUBLIC)
                        && md.getModifiers().stream().anyMatch(m -> m.getKeyword() == Modifier.Keyword.STATIC)
                        && md.getParameters().size() == 1
                        && md.getParameters().get(0).getTypeAsString().equals("String[]"));
    }

    private static double calScore(int killedCnt, int totalNum) {
        return ((double) killedCnt / (double) totalNum) * 100;
    }
}
